feat: setup of top- and bottombar

This commit is contained in:
Emma Vandewalle 2024-08-23 07:24:58 +00:00
parent c2277d0c15
commit a7d0d746cb
11 changed files with 394 additions and 0 deletions

View file

@ -1,11 +1,36 @@
package be.re.writand.screens.editor package be.re.writand.screens.editor
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import be.re.writand.screens.editor.bottom.BottomEditorBar
import be.re.writand.screens.editor.editorspace.EditorSpace
import be.re.writand.screens.editor.top.TopEditorBar
/**
* Composable presenting the full screen when IDE is open. This holds all the different composables
* that come together on the screen.
*/
@Composable @Composable
fun EditorScreen( fun EditorScreen(
navHostController: NavHostController navHostController: NavHostController
) { ) {
Scaffold(
topBar = {
TopEditorBar()
},
bottomBar = {
BottomEditorBar()
}
) {
Row(modifier = Modifier.padding(it)) {
// TODO: show filetree when open
EditorSpace()
}
}
} }

View file

@ -0,0 +1,38 @@
package be.re.writand.screens.editor.bottom
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import be.re.writand.screens.components.WText
/**
* Bar component that represents the bottom of the editor itself.
* @param[vM] the viewmodel used for this screen for button clicks and showing the current row/column.
*/
@Composable
fun BottomEditorBar(
vM: BottomEditorBarViewModel = hiltViewModel()
) {
val row = vM.row.collectAsState()
val column = vM.column.collectAsState()
Column {
HorizontalDivider()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 15.dp),
horizontalArrangement = Arrangement.End
) {
WText(text = "${row.value}:${column.value}")
}
}
}

View file

@ -0,0 +1,24 @@
package be.re.writand.screens.editor.bottom
import be.re.writand.screens.WViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Viewmodel to be used by BottomEditorBar that handles the buttons and what the current
* row/column is.
*/
@HiltViewModel
class BottomEditorBarViewModel : WViewModel() {
private val _row = MutableStateFlow(0)
val row: StateFlow<Int> = _row.asStateFlow()
private val _column = MutableStateFlow(0)
var column: StateFlow<Int> = _column.asStateFlow()
// TODO: update the row and column when cursor is moved or user types
}

View file

@ -0,0 +1,103 @@
package be.re.writand.screens.editor.editorspace
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import be.re.writand.screens.components.WLoadingIndicator
import be.re.writand.screens.components.WText
/**
* The editor space itself where the user can edit the currently open file.
* @param[vM] the viewmodel used to handle input changes to the open file.
*/
@Composable
fun EditorSpace(
vM: EditorSpaceViewModel = hiltViewModel()
) {
val uiState by vM.uiState.collectAsState()
when (val value = uiState) {
EditorSpaceUiState.Loading -> {
WLoadingIndicator()
}
is EditorSpaceUiState.Success -> {
// using a mutable state to store the number of lines
var linesText by remember { mutableIntStateOf(value.fileContent.count { it == '\n' } + 1) }
// the scrolling state of both text fields
val linesTextScroll = rememberScrollState()
val scriptTextScroll = rememberScrollState()
// synchronize scrolling
LaunchedEffect(linesTextScroll.value) {
scriptTextScroll.scrollTo(linesTextScroll.value)
}
LaunchedEffect(scriptTextScroll.value) {
linesTextScroll.scrollTo(scriptTextScroll.value)
}
BasicTextField(
modifier = Modifier
.fillMaxHeight()
.padding(start = 25.dp)
.width(12.dp * linesText.toString().length)
.verticalScroll(linesTextScroll),
value = IntRange(1, linesText).joinToString(separator = "\n"),
readOnly = true,
textStyle = TextStyle(color = MaterialTheme.colorScheme.secondary),
onValueChange = {}
)
VerticalDivider(
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 8.dp),
color = MaterialTheme.colorScheme.secondary
)
// actual textField
BasicTextField(
modifier = Modifier
.fillMaxHeight()
// this is a hack to prevent this https://stackoverflow.com/questions/76287857/when-parent-of-textfield-is-clickable-hardware-enter-return-button-triggers-its
.onKeyEvent { it.type == KeyEventType.KeyUp && it.key == Key.Enter }
.verticalScroll(scriptTextScroll),
value = value.fileContent,
readOnly = false,
textStyle = TextStyle(color = MaterialTheme.colorScheme.onPrimary),
onValueChange = { textFieldValue ->
val nbLines = textFieldValue.count { it == '\n' } + 1
if (nbLines != linesText) linesText = nbLines
vM.onTextChange(textFieldValue)
},
)
}
is EditorSpaceUiState.Failed -> {
WText(text = value.message)
}
}
}

View file

@ -0,0 +1,14 @@
package be.re.writand.screens.editor.editorspace
/**
* Interface to present the 3 differences stages of getting the contents of file:
* - [Loading]: the content is pending.
* - [Success]: the file content is ready, and is given in a String.
* - [Failed]: unable to get the content from the file, give back an error message.
*/
sealed interface EditorSpaceUiState {
object Loading : EditorSpaceUiState
data class Success(val fileContent: String) : EditorSpaceUiState
data class Failed(val message: String) : EditorSpaceUiState
}

View file

@ -0,0 +1,35 @@
package be.re.writand.screens.editor.editorspace
import be.re.writand.screens.WViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
/**
* Viewmodel to be used by EditorSpace to handle file changes and keep the ui state in memory.
*/
@HiltViewModel
class EditorSpaceViewModel : WViewModel() {
private val _uiState: MutableStateFlow<EditorSpaceUiState> =
MutableStateFlow(EditorSpaceUiState.Loading)
val uiState: StateFlow<EditorSpaceUiState> = _uiState
init {
launchCatching {
// TODO: call use-case that gets the String of the file
val file: String = "print('Hello World!')\nprint('second line')\n"
if (file.isNotEmpty()) {
_uiState.value = EditorSpaceUiState.Success(file)
} else {
_uiState.value = EditorSpaceUiState.Failed("The file could not be loaded.")
}
}
}
fun onTextChange(newValue: String) {
_uiState.value = EditorSpaceUiState.Success(newValue)
}
}

View file

@ -0,0 +1,97 @@
package be.re.writand.screens.editor.top
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import be.re.writand.R
import be.re.writand.screens.components.WText
/**
* Composable representing the top bar of the editor.
* @param[vM] the viewmodel used to handle button clicks and show if file is saved or not.
*/
@Composable
fun TopEditorBar(
vM: TopEditorBarViewModel = hiltViewModel()
) {
val openFile = vM.currentFile.collectAsState()
val isSaved = vM.isSaved.collectAsState()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 10.dp)
.sizeIn(minHeight = 50.dp, maxHeight = 150.dp)
.border(
1.dp,
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(10.dp)
)
.clip(shape = RoundedCornerShape(10.dp))
.background(color = MaterialTheme.colorScheme.tertiary),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { vM.onFileMenuClick() },
modifier = Modifier.padding(start = 10.dp),
) {
Icon(
modifier = Modifier.size(35.dp),
imageVector = Icons.Default.Menu,
contentDescription = "Menu"
)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
WText(text = openFile.value)
IconButton(onClick = { vM.onSaveFile() }) {
if (isSaved.value) {
Icon(
modifier = Modifier.size(25.dp),
painter = painterResource(id = R.drawable.saved),
contentDescription = "Save"
)
} else {
Icon(
modifier = Modifier.size(25.dp),
painter = painterResource(id = R.drawable.not_saved),
contentDescription = "Save"
)
}
}
}
IconButton(
onClick = { vM.onTerminalClick() },
modifier = Modifier.padding(end = 10.dp)
) {
Icon(
modifier = Modifier.size(35.dp),
painter = painterResource(id = R.drawable.terminal),
contentDescription = "Terminal"
)
}
}
}

View file

@ -0,0 +1,34 @@
package be.re.writand.screens.editor.top
import be.re.writand.screens.WViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* View model to be used by TopEditorBar handling the buttons and saving of the file.
*/
@HiltViewModel
class TopEditorBarViewModel : WViewModel() {
private val _saved = MutableStateFlow(true)
val isSaved: StateFlow<Boolean> = _saved.asStateFlow()
private val _file = MutableStateFlow("hello_world.py")
var currentFile: StateFlow<String> = _file.asStateFlow()
fun onFileMenuClick() {
// TODO: Open or close the filetree
}
fun onTerminalClick() {
// TODO: A terminal should pop up
}
fun onSaveFile() {
// TODO: save the actual file with the filetree api
_saved.value = true
}
}

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
<path android:fillColor="#00000000" android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0" android:strokeColor="@color/black" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
<path android:fillColor="#00000000" android:pathData="M21.801,10A10,10 0,1 1,17 3.335" android:strokeColor="@color/black" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000" android:pathData="m9,11 l3,3L22,4" android:strokeColor="@color/black" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M6,9a0.5,0.5 0,0 1,0.5 -0.5h3a0.5,0.5 0,0 1,0 1h-3A0.5,0.5 0,0 1,6 9M3.854,4.146a0.5,0.5 0,1 0,-0.708 0.708L4.793,6.5 3.146,8.146a0.5,0.5 0,1 0,0.708 0.708l2,-2a0.5,0.5 0,0 0,0 -0.708z"
android:fillColor="@color/black"/>
<path
android:pathData="M2,1a2,2 0,0 0,-2 2v10a2,2 0,0 0,2 2h12a2,2 0,0 0,2 -2L16,3a2,2 0,0 0,-2 -2zM14,2a1,1 0,0 1,1 1v10a1,1 0,0 1,-1 1L2,14a1,1 0,0 1,-1 -1L1,3a1,1 0,0 1,1 -1z"
android:fillColor="@color/black"/>
</vector>